New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wallet: Make IsTrusted scan parents recursively #16766
Conversation
I added a cache which works per call/across calls, but does not store the results semi-permanently. This should reduce DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected. |
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
Would be nice to have a test for this or update the |
I updated the tests to pass. In the test, we have two nodes with a balance of 50 each and then: Send 40 from node A to node B. Then we would check the balances. Previously, we would immediately report a balance of 30 on node B as unconfirmed and trusted. Now, we show that balance as unconfirmed. The balance is indeed "trusted" and "confirmed" insofar as removing the mempool transactions would return at least that much money. But the current software marks it as unconfirmed because the 'taint' tracking of transaction trust for summing balances doesn't consider which inputs belong to a user. I think the new behavior is correct; we shouldn't be treating those funds as confirmed. If you want to rely on that specific UTXO existing which has given you that balance, you cannot, as a third party spending the other input would destroy that unconfirmed. Overall I think that this points to a general concept worth exploring as a revision to the categories "unconfirmed" and "confirmed", to have "free cash", "locked", and "unconfirmed". Free cash can be spent at will, locked funds are balances that are guaranteed to exist but we don't have a confirmed UTXO for yet, and unconfirmed are funds we may or may not receive. I think this is out of scope of this PR though, but is worth further thought. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
Test failure unrelated it seems? In assumevalid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK.
Test failure unrelated it seems? In assumevalid
Yup, just built and all tests are green.
src/wallet/wallet.cpp
Outdated
for (const auto& entry : mapWallet) | ||
{ | ||
const CWalletTx& wtx = entry.second; | ||
const bool is_trusted{wtx.IsTrusted(*locked_chain)}; | ||
const bool is_trusted{wtx.IsTrusted(*locked_chain, trustedParents)}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the overloaded IsTrusted
this could be reverted? Same below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope -- because I'm caching the trust across the entire wallet (outside the loop) rather than just a single call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, missed this is in a loop 🤦♂
src/wallet/wallet.cpp
Outdated
if (pwallet->IsMine(parentOut) != ISMINE_SPENDABLE) | ||
return false; | ||
// If we've already trusted this parent, continue | ||
if (trustedParents.count(parent->GetHash())) | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, join with line above - to follow correct format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it; went ahead and did this in the entire function (rather than leave it half-styled)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that was the idea.
src/wallet/wallet.cpp
Outdated
return IsTrusted(locked_chain, s); | ||
} | ||
|
||
bool CWalletTx::IsTrusted(interfaces::Chain::Lock& locked_chain, std::set<uint256>& trustedParents) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, snake case - header too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we change styles?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We settled on a variable naming style in 2017: 47d8441
Do you expect a performance penalty for big wallets? |
Yes and No. Yes: No: Note that a third party can't force a long-chain lookup on you without your participation, because you're checking IsMine at each level. So an unconfirmed spend to you will look back only until it's a transaction with another participant that's unconfirmed. It's also possible to use an unordered_set here -- but perhaps that's better done as something across the module (std::set/map is used extensively) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JeremyRubin thanks for the detailed response! Some comments follow.
if (!locked_chain.checkFinalTx(*tx)) { | ||
return false; | ||
} | ||
if (!locked_chain.checkFinalTx(*tx)) return false; | ||
int nDepth = GetDepthInMainChain(locked_chain); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something but I think you could avoid calling GetDepthInMainChain
for parents if nDepth >= 1
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, but we already don't recurse on trusted parents right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh!
@@ -576,6 +576,7 @@ class CWalletTx | |||
|
|||
bool InMempool() const; | |||
bool IsTrusted(interfaces::Chain::Lock& locked_chain) const; | |||
bool IsTrusted(interfaces::Chain::Lock& locked_chain, std::set<uint256>& trusted_parents) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, should be private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I'd rather just get rid of the other function as usually you'd want to share the cache across IsTrusted calls...
Slightly more agressive change but maybe it's ok
// Check that this specific input being spent is trusted | ||
if (pwallet->IsMine(parentOut) != ISMINE_SPENDABLE) return false; | ||
// If we've already trusted this parent, continue | ||
if (trusted_parents.count(parent->GetHash())) continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could .insert()
here, and use the result value .second
to know whether it was missing and then call parent->IsTrusted
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little uncomfortable with that design because we then need to remove ourselves from the set if the recursion returns false.
It's also a bit weird because we add ourselves to the trusted set before recursion so technically there are elements not known to be trusted in the set. This is safe because the element is a child, but it's odd.
But your balance isn't specific UTXOs. Users don't care about UTXOs, just total bitcoins. 30 BTC should show as confirmed and trusted... |
Then it's a mistake to compute balance from summing IsTrusted, but we shouldn't rely on broken IsTrusted behavior to compute Balance. Bear in mind for the specific situation you're thinking of, there are also edge cases where we over report correct balance. |
Was the test you changed a good example of how this can happen or did you have other examples in mind? Would it be worth to have an explicit test for that? |
for (const auto& entry : mapWallet) | ||
{ | ||
const CWalletTx& wtx = entry.second; | ||
const bool is_trusted{wtx.IsTrusted(*locked_chain)}; | ||
const bool is_trusted{wtx.IsTrusted(*locked_chain, trusted_parents)}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would prefer to keep using the old interface here and in 2488, 3784 until the trusted_parents
are actually used/shared.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by "actually used/shared"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean for now, you could leave this line unchanged since trusted_parents
is not used outside of IsTrusted
. I know you eventually want to share the parents for optimization but I would not change this line until that is the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what the "eventually" or "actually" part is... this is what is done by the code already. Note that trusted_parents lives outside the for loop so is shared across calls.
And it's less so optimization and more so anti-DoS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I misunderstood at which level the caching is happening, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 58cb227. This seems right. It doesn't seem good to trust a child transaction if the parents aren't trusted. But I don't understand what originally motivated this change. Is there a prototypical example of a case where this would be an obvious improvement? The test example in #16766 (comment) seems more like arguably correct behavior with not a great outcome. I wouldn't mind seeing:
- The PR description updated with more motivation, and maybe a motivating example
- A release note to warn users and avoid surprises from balances being computed differently than before
I like the proposed free/locked/unconfirmed idea, too. I hope future changes could allow figuring out a free+locked value, which has seemed to me like a useful thing to have before #15010 (comment).
@@ -112,10 +112,10 @@ def run_test(self): | |||
def test_balances(*, fee_node_1=0): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "Update wallet_balance.py test to reflect new behavior" (bee1dc9)
You also have a really nice description of the test setup in #16766 (comment) that could be nice to incorporate as a comment or link in the test_balances
function.
Hi @ryanofsky, thanks for the review! This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own. I'll add to the release notes and to the tests comments. |
Co-Authored-By: MarcoFalke <falke.marco@gmail.com>
58cb227
to
4671fc3
Compare
I had to rebase to clear out the release notes; code hasn't changed except the comment in wallet_balances.py. I have also updated the PR message. re-ack when you have a moment @ryanofsky! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 4671fc3. Changes since last review: 2 new commits adding suggested release note and python test comment, also a clean rebase with no changes to the earlier commits. The PR description is more comprehensive now, too. Looks good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK, this was previously a non-tested bug. Don't know about the DoS protection, see comment.
Overall I think that this points to a general concept worth exploring as a revision to the categories "unconfirmed" and "confirmed", to have "free cash", "locked", and "unconfirmed". Free cash can be spent at will, locked funds are balances that are guaranteed to exist but we don't have a confirmed UTXO for yet, and unconfirmed are funds we may or may not receive. I think this is out of scope of this PR though, but is worth further thought
Agree, UTXO state management is pretty unclear currently, I was thinking about this on the spending-side to rework abandontransaction
to something like abandonutxo
with the given nomenclature, committed UTXO included in one or more tx, spent UTXO include in a confirm tx, where user should be able to unlock committed UTXO if they are his. Conflicts tracking should stay at the transaction-level. On the receiving-side, not sure about the difference between locked and unconfirmed
@@ -2294,6 +2294,12 @@ bool CWalletTx::InMempool() const | |||
} | |||
|
|||
bool CWalletTx::IsTrusted(interfaces::Chain::Lock& locked_chain) const | |||
{ | |||
std::set<uint256> s; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I don't know about the need of DoS protection, can't we rely on mempool DEFAULT_ANCESTOR_LIMIT there ? A transaction to be trusted has to be in-mempool (among other standards), so their unconfirmed parent too, at first parent hit which is not we should return. What's th worst complexity for a graph of 25 parents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even so, you can imagine a case where we have a single input single output chain of 25 taking:
sum n = 0 to N n = n * (n-1) /2 = O(N^2).
Which for 25 = 625 iterations.
Imagine what this would be for a tree with no descendants limit!
The caching guarantees that it is O(N*log(N)) worst case (could be improved possibly by switching to unordered_set... but it's worth benchmarking since we will usually have a very small set)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the worst case is O(a^n), where a is the number of inputs/output per transaction, and n is the ancestor limit. Simply build a tx graph where every transaction spends a outputs of the previous layer, in a different inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay assuming complexity O(a^n), standard txn are limited by MAX_STANDARD_TX_WEIGHT and 148-bytes input, that gives you 548 inputs/outputs on every tx layer so something like 13700 iterations... Would be interesting to know how IsTrusted
behaves on such computation order, but the fees burn for a DoS attacker seems pretty high.
Imagine what this would be for a tree with no descendants limit!
Not sure why descendants limit matters there?
Anyway, DoS protection is simple enough to get it for peace of mind.
src/wallet/wallet.cpp
Outdated
@@ -2420,10 +2420,11 @@ CWallet::Balance CWallet::GetBalance(const int min_depth, bool avoid_reuse) cons | |||
{ | |||
auto locked_chain = chain().lock(); | |||
LOCK(cs_wallet); | |||
std::set<uint256> trustedParents; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you go with DoS protection, can't you go further and reuse the cached set of trusted parents between WalletTxToJSON call in listtransactions
and listsinceblock
, they are all after chain locks so shouldn't be an issue ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly, but because it's through the interfaced call I didn't want to scope creep to include that. But it's maybe worth a follow up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really IMO but if DoS protection is worth it, it should take precedence on creeping into interfaced call.
@@ -85,6 +85,7 @@ Wallet | |||
------ | |||
|
|||
- The wallet now by default uses bech32 addresses when using RPC, and creates native segwit change outputs. | |||
- The way that output trust was computed has been fixed in #16766, which impacts confirmed/unconfirmed balance status and coin selection. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe precise how the trust computation has been changed "to evaluate trustiness of outpoint spent by parent and so recursively"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what level of detail is best for the release note; will leave up to maintainer discretion. Since it can be nuanced, if someone is relying on this it's best that they read the PR.
Code Review ACK 4671fc3, maybe extend DoS protection in a follow-up PR. |
Code review ACK 4671fc3 Thanks for adding the extended description in the test. Good idea @ryanofsky ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 4671fc3.
b49dcbe could be avoided (by fixing the respective commits in the first place) and 8f174ef could be discarded since it's kind of unrelated. I guess it's not a big deal considering the existing ACK.
Looking into IsTrusted
, couldn't we change it to not require cs_main
(just cs_wallet
)?
4671fc3 Expand on wallet_balance.py comment from #16766 (Jeremy Rubin) 91f3073 Update release notes to mention changes to IsTrusted and impact on wallet (Jeremy Rubin) 8f174ef Systematize style of IsTrusted single line if (Jeremy Rubin) b49dcbe update variable naming conventions for IsTrusted (Jeremy Rubin) 5ffe0d1 Update comment in test/functional/wallet_balance.py (Jeremy Rubin) a550c58 Update wallet_balance.py test to reflect new behavior (Jeremy Rubin) 5dd7da4 Reuse trustedParents in looped calls to IsTrusted (Jeremy Rubin) 595f09d Cache tx Trust per-call to avoid DoS (Jeremy Rubin) dce032c Make IsTrusted scan parents recursively (Jeremy Rubin) Pull request description: This slightly modifies the behavior of IsTrusted to recursively check the parents of a transaction. Otherwise, it's possible that a parent is not IsTrusted but a child is. If a parent is not trusted, then a child should not be either. This recursive scan can be a little expensive, so ~it might be beneficial to have a way of caching IsTrusted state, but this is a little complex because various conditions can change between calls to IsTrusted (e.g., re-org).~ I added a cache which works per call/across calls, but does not store the results semi-permanently. Which reduces DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected. This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own by fixing a long standing wallet bug. The test wallet_balance.py has been corrected to meet the new behavior. The below comment, reproduced, explains what the issue is and the edge cases that can arise before this change. # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to #16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After #16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after #16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post #16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. The release notes have been updated to note the new behavior. ACKs for top commit: ariard: Code Review ACK 4671fc3, maybe extend DoS protection in a follow-up PR. fjahr: Code review ACK 4671fc3 ryanofsky: Code review ACK 4671fc3. Changes since last review: 2 new commits adding suggested release note and python test comment, also a clean rebase with no changes to the earlier commits. The PR description is more comprehensive now, too. Looks good! promag: Code review ACK 4671fc3. Tree-SHA512: 6b183ff425304fef49724290053514cb2770f4a2350dcb83660ef24af5c54f7c4c2c345b0f62bba60eb2d2f70625ee61a7fab76a7f491bb5a84be5c4cc86b92f
Concept ACK, this looks ready with 4 review ACKs |
re: #16766 (review) from @promag
It's not trivial because of the GetDepthInMainChain and checkFinalTx calls, but #16426 does this |
4671fc3 Expand on wallet_balance.py comment from bitcoin#16766 (Jeremy Rubin) 91f3073 Update release notes to mention changes to IsTrusted and impact on wallet (Jeremy Rubin) 8f174ef Systematize style of IsTrusted single line if (Jeremy Rubin) b49dcbe update variable naming conventions for IsTrusted (Jeremy Rubin) 5ffe0d1 Update comment in test/functional/wallet_balance.py (Jeremy Rubin) a550c58 Update wallet_balance.py test to reflect new behavior (Jeremy Rubin) 5dd7da4 Reuse trustedParents in looped calls to IsTrusted (Jeremy Rubin) 595f09d Cache tx Trust per-call to avoid DoS (Jeremy Rubin) dce032c Make IsTrusted scan parents recursively (Jeremy Rubin) Pull request description: This slightly modifies the behavior of IsTrusted to recursively check the parents of a transaction. Otherwise, it's possible that a parent is not IsTrusted but a child is. If a parent is not trusted, then a child should not be either. This recursive scan can be a little expensive, so ~it might be beneficial to have a way of caching IsTrusted state, but this is a little complex because various conditions can change between calls to IsTrusted (e.g., re-org).~ I added a cache which works per call/across calls, but does not store the results semi-permanently. Which reduces DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected. This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own by fixing a long standing wallet bug. The test wallet_balance.py has been corrected to meet the new behavior. The below comment, reproduced, explains what the issue is and the edge cases that can arise before this change. # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to bitcoin#16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After bitcoin#16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after bitcoin#16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post bitcoin#16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. The release notes have been updated to note the new behavior. ACKs for top commit: ariard: Code Review ACK 4671fc3, maybe extend DoS protection in a follow-up PR. fjahr: Code review ACK 4671fc3 ryanofsky: Code review ACK 4671fc3. Changes since last review: 2 new commits adding suggested release note and python test comment, also a clean rebase with no changes to the earlier commits. The PR description is more comprehensive now, too. Looks good! promag: Code review ACK 4671fc3. Tree-SHA512: 6b183ff425304fef49724290053514cb2770f4a2350dcb83660ef24af5c54f7c4c2c345b0f62bba60eb2d2f70625ee61a7fab76a7f491bb5a84be5c4cc86b92f
Summary: Expand on wallet_balance.py comment from bitcoin/bitcoin#16766 (Jeremy Rubin) Update release notes to mention changes to IsTrusted and impact on wallet (Jeremy Rubin) Systematize style of IsTrusted single line if (Jeremy Rubin) update variable naming conventions for IsTrusted (Jeremy Rubin) Update comment in test/functional/wallet_balance.py (Jeremy Rubin) Update wallet_balance.py test to reflect new behavior (Jeremy Rubin) Reuse trustedParents in looped calls to IsTrusted (Jeremy Rubin) Cache tx Trust per-call to avoid DoS (Jeremy Rubin) Make IsTrusted scan parents recursively (Jeremy Rubin) Pull request description: This slightly modifies the behavior of IsTrusted to recursively check the parents of a transaction. Otherwise, it's possible that a parent is not IsTrusted but a child is. If a parent is not trusted, then a child should not be either. This recursive scan can be a little expensive, so ~it might be beneficial to have a way of caching IsTrusted state, but this is a little complex because various conditions can change between calls to IsTrusted (e.g., re-org).~ I added a cache which works per call/across calls, but does not store the results semi-permanently. Which reduces DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected. This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own by fixing a long standing wallet bug. The test wallet_balance.py has been corrected to meet the new behavior. The below comment, reproduced, explains what the issue is and the edge cases that can arise before this change. # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to #16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After #16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after #16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post #16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. The release notes have been updated to note the new behavior. --- Backport of Core [[bitcoin/bitcoin#16766 | PR16766]] Test Plan: ninja check check-functional Reviewers: #bitcoin_abc, jasonbcox Reviewed By: #bitcoin_abc, jasonbcox Subscribers: jasonbcox Differential Revision: https://reviews.bitcoinabc.org/D7695
4671fc3 Expand on wallet_balance.py comment from bitcoin#16766 (Jeremy Rubin) 91f3073 Update release notes to mention changes to IsTrusted and impact on wallet (Jeremy Rubin) 8f174ef Systematize style of IsTrusted single line if (Jeremy Rubin) b49dcbe update variable naming conventions for IsTrusted (Jeremy Rubin) 5ffe0d1 Update comment in test/functional/wallet_balance.py (Jeremy Rubin) a550c58 Update wallet_balance.py test to reflect new behavior (Jeremy Rubin) 5dd7da4 Reuse trustedParents in looped calls to IsTrusted (Jeremy Rubin) 595f09d Cache tx Trust per-call to avoid DoS (Jeremy Rubin) dce032c Make IsTrusted scan parents recursively (Jeremy Rubin) Pull request description: This slightly modifies the behavior of IsTrusted to recursively check the parents of a transaction. Otherwise, it's possible that a parent is not IsTrusted but a child is. If a parent is not trusted, then a child should not be either. This recursive scan can be a little expensive, so ~it might be beneficial to have a way of caching IsTrusted state, but this is a little complex because various conditions can change between calls to IsTrusted (e.g., re-org).~ I added a cache which works per call/across calls, but does not store the results semi-permanently. Which reduces DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected. This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own by fixing a long standing wallet bug. The test wallet_balance.py has been corrected to meet the new behavior. The below comment, reproduced, explains what the issue is and the edge cases that can arise before this change. # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to bitcoin#16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After bitcoin#16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after bitcoin#16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post bitcoin#16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. The release notes have been updated to note the new behavior. ACKs for top commit: ariard: Code Review ACK 4671fc3, maybe extend DoS protection in a follow-up PR. fjahr: Code review ACK 4671fc3 ryanofsky: Code review ACK 4671fc3. Changes since last review: 2 new commits adding suggested release note and python test comment, also a clean rebase with no changes to the earlier commits. The PR description is more comprehensive now, too. Looks good! promag: Code review ACK 4671fc3. Tree-SHA512: 6b183ff425304fef49724290053514cb2770f4a2350dcb83660ef24af5c54f7c4c2c345b0f62bba60eb2d2f70625ee61a7fab76a7f491bb5a84be5c4cc86b92f
This slightly modifies the behavior of IsTrusted to recursively check the parents of a transaction. Otherwise, it's possible that a parent is not IsTrusted but a child is. If a parent is not trusted, then a child should not be either.
This recursive scan can be a little expensive, so
it might be beneficial to have a way of caching IsTrusted state, but this is a little complex because various conditions can change between calls to IsTrusted (e.g., re-org).I added a cache which works per call/across calls, but does not store the results semi-permanently. Which reduces DoS risk of this change. There is no risk of untrusted parents causing a resource exploitation, as we immediately return once that is detected.This is a change that came up as a bug-fix esque change while working on OP_SECURETHEBAG. You can see the branch where this change is important here: https://github.com/bitcoin/bitcoin/compare/master...JeremyRubin:stb-with-rpc?expand=1. Essentially, without this change, we can be tricked into accepting an OP_SECURETHEBAG output because we don't properly check the parents. As this was a change which, on its own, was not dependent on OP_SECURETHEBAG, I broke it out as I felt the change stands on its own by fixing a long standing wallet bug.
The test wallet_balance.py has been corrected to meet the new behavior. The below comment, reproduced, explains what the issue is and the edge cases that can arise before this change.
The release notes have been updated to note the new behavior.